将架构蓝图项目迁移至 Jetpack Compose
作者 / Manuel Vivo, Android DevRel @ Google
在我们努力实现应用架构指南现代化的过程中,我们希望尝试各种用户界面模式,了解哪个模式最有效,找出替代方案之间的相似性和差异,并最终将这些内容整合为最佳实践。
应用架构指南 https://developer.android.google.cn/topic/architecture
Android 架构蓝图
https://github.com/android/architecture-samples
△ 架构蓝图应用演示
Jetpack Compose State API https://developer.android.google.cn/jetpack/compose/state 单向数据流模式 https://developer.android.google.cn/jetpack/guide/ui-layer#udf
LiveData https://developer.android.google.cn/topic/libraries/architecture/livedata
应用架构指南 https://developer.android.google.cn/jetpack/guide 数据层 https://developer.android.google.cn/jetpack/guide/data-layer 网域层 https://developer.android.google.cn/jetpack/guide/domain-layer
https://github.com/android/architecture-samples/tree/dev-compose
✍️ 规划逐步迁移
https://developer.android.google.cn/jetpack/compose/navigation
导航 https://developer.android.google.cn/guide/navigation 互操作性 https://developer.android.google.cn/jetpack/compose/navigation#interoperability
逐步迁移的目的是减少代码审查工作量,并在整个迁移过程中保持产品可交付。迁移计划涉及三个步骤:
将每个屏幕的内容迁移至 Compose。每个屏幕均可单独迁移至 Compose,包括其界面测试。然后 Fragment 将成为每个已迁移屏幕的容器。
将应用迁移至 Navigation Compose (此操作会移除项目中的所有 Fragment) 并将 Activity 界面逻辑迁移至基于 Composable。端到端测试也会在此时迁移。
移除 View 系统依赖项。
将 Statistics 迁移至 Compose https://github.com/android/architecture-samples/pull/821 将 AddEditTask 屏幕迁移至 Compose https://github.com/android/architecture-samples/pull/823 将 TaskDetail 迁移至 Compose https://github.com/android/architecture-samples/pull/822 将 Tasks 迁移至 Compose https://github.com/android/architecture-samples/pull/826 将 Activity 和 NavGraph 迁移至 Compose https://github.com/android/architecture-samples/pull/827 移除未使用的 View 依赖 https://github.com/android/architecture-samples/pull/827/commits/2810a37c479ef4b23b4cabf095c55df7b342235e
△ 我们如何将蓝图逐步迁移至 Compose
💡 迁移重点
🧪 界面测试
将 Compose 添加到应用后,断言 Compose 界面的测试需要使用 Compose 测试 API:
对于屏幕级别的界面测试,我们不使用 launchFragmentInContainer<FragmentType> API,而是使用 createAndroidComposeRule<ComponentActivity> API,这样我们可以在测试中捕获字符串资源。这些测试可在 Espresso 和 Robolectric 中运行。因为 Compose 已经可为所有这一切提供支持,所以无需任何额外改动。例如,您可以比较 AddEditTaskFragmentTest 中已迁移至 AddEditTaskScreenTest 的代码。请注意,如果您使用 ComponentActivity,那么需要依赖 androidx.compose.ui:ui-test-manifest 组件。
launchFragmentInContainer<FragmentType> https://developer.android.google.cn/guide/fragments/test#create createAndroidComposeRule<ComponentActivity> https://developer.android.google.cn/jetpack/compose/testing AddEditTaskFragmentTest https://github.com/android/architecture-samples/blob/653a563e9fe0874b4ae3fba539ce4b6518a2f796/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskFragmentTest.kt AddEditTaskScreenTest https://github.com/manuelvicnt/architecture-samples/blob/8a203594541b25e5eec2daac63415c05884242ad/app/src/sharedTest/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreenTest.kt androidx.compose.ui:ui-test-manifest https://developer.android.google.cn/jetpack/compose/testing#setup
https://github.com/manuelvicnt/architecture-samples/blob/249a636ea9a3f16aab5c284e3245069ef56a557f/app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/tasks/AppNavigationTest.kt
🤙 ViewModel 事件
处理 ViewModel 事件
https://developer.android.google.cn/jetpack/guide/ui-layer/events#handle-viewmodel-events
事件封装容器
https://github.com/android/architecture-samples/blob/8e1e0527a0d043b41da58925a39fb8e03d62829a/app/src/main/java/com/example/android/architecture/blueprints/todoapp/Event.kt
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
class AddEditTaskViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
- private val _snackbarText = MutableLiveData<Event<Int>>()
- val snackbarText: LiveData<Event<Int>> = _snackbarText
+ private val _snackbarText = MutableLiveData<Int?>()
+ val snackbarText: LiveData<Int?> = _snackbarText
+ fun snackbarMessageShown() {
+ _snackbarText.value = null
+ }
}
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// FRAGMENTS CODE CONSUMING THE EVENT WRAPPER SOLUTION
- class AddEditTaskFragment : Fragment() {
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- ...
- viewModel.snackbarText.observe(
- lifecycleOwner,
- Observer { event ->
- event.getContentIfNotHandled()?.let {
- showSnackbar(context.getString(it), Snackbar.LENGTH_SHORT)
- }
- }
- )
- }
- }
// COMPOSE CODE CONSUMING USER MESSAGES AS STATE
// State holder for the AddEditTask composable.
// This class handles AddEditTask's UI elements' state and UI logic.
+ class AddEditTaskState(...) {
+ init {
+ // Listen for snackbar messages
+ viewModel.snackbarText.observe(viewLifecycleOwner) { snackbarMessage ->
+ if (snackbarMessage != null) {
+ // If there's a previous message showing on the screen
+ // stop showing it in favor of the new one to be displayed
+ currentSnackbarJob?.cancel()
+ val snackbarText = context.getString(snackbarMessage)
+ currentSnackbarJob = coroutineScope.launch {
+ scaffoldState.snackbarHostState.showSnackbar(snackbarText)
+ viewModel.snackbarMessageShown()
+ }
+ }
+ }
+ }
逻辑类型
https://developer.android.google.cn/jetpack/guide/ui-layer#logic-types
状态和逻辑的类型
https://developer.android.google.cn/jetpack/compose/state#types-of-state-and-logic
AddEditTaskState
https://github.com/manuelvicnt/architecture-samples/blob/88cf650fd1759486cce198878b5cf08e823012dc/app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskState.kt
👌 请优先确保应用正确性
重构期间,您可能很想把手上的所有内容迁移到 Compose。虽然这么做完全没问题,但您不应牺牲应用的用户体验或正确性。逐步迁移的全部意义在于,让应用始终处于可交付状态。
在将一些屏幕迁移到 Compose 时,我们也遇到了这种情况。我们不想同时进行过多迁移,所以在从事件封装容器迁移 "之前",先将一些屏幕迁移到了 Compose。与其在 Compose 中处理事件封装容器,获得不够理想的体验,不如继续在 Fragment 中处理这些消息,而屏幕的其他代码则使用 Compose 实现。例如,您可以参考迁移过程中 TasksFragment 的状态:
https://github.com/manuelvicnt/architecture-samples/blob/249a636ea9a3f16aab5c284e3245069ef56a557f/app/src/main/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksFragment.kt
🧐 挑战
不是所有步骤都像看上去那么顺利。尽管将 Fragment 内容转换为 Compose 很简单,但从 Navigation Fragment 迁移到 Navigation Compose 需要花费更多的时间和心思。
我们有必要从各方面扩展和改进指南,让迁移到 Compose 的过程更加轻松。这项工作引起了广泛讨论,我们希望很快制定出这方面的全新指南!🎊
我在初次使用 Navigation ✋ 并处理向 Navigation Compose 迁移的问题时,面临了以下挑战:
文档中没有任何代码显示如何使用可选参数进行导航!多亏有 Tivi 的导航图,我才找到办法解决这个问题。您可以关注此问题并改进文档:
https://issuetracker.google.com/226103829
Tivi 的导航图
https://github.com/chrisbanes/tivi/blob/main/app/src/main/java/app/tivi/AppNavigation.kt
从基于 XML 的导航图和 SafeArgs 迁移到 Kotlin DSL 应该是一项简单的机械式任务。但对我来说这项任务并不轻松,因为我并没有参与初始实现。一些有关如何正确操作的指南本应对我有所帮助。您可以关注此问题并改进文档:
https://issuetracker.google.com/226315955
第三点与其说是挑战,不如说这就是一个问题。说到导航,NavigationUI 已经为您做了一些工作。由于 Compose 中不存在该界面,您需要注意这一点,并手动实现。例如,在 Drawer 屏幕之间导航时,保持后退堆栈的清洁需要特殊的 NavigationOptions (请参考示例)。文档中已经讲到了这一点,但您需要意识到自己需要这么做!
使用 NavigationUI 更新界面组件
https://developer.android.google.cn/guide/navigation/navigation-ui
示例: TodoNavigation
https://github.com/android/architecture-samples/blob/dev-compose/app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavigation.kt#L79
文档: 与底部导航栏集成
https://developer.android.google.cn/jetpack/compose/navigation#bottom-nav
🧑🏫 小结
总的来说,从 Navigation Fragment 迁移到 Navigation Compose 是一项有趣的工作!有意思的是,我们花在等待同行审查上的时间,比迁移项目本身的时间还要多!制定迁移计划并让每个人都切实理解它,无疑有助于尽早确定期望结果,并提醒同事注意即将到来的漫长审查。
希望这篇文章对您有所帮助,让您了解了我们迁移到 Compose 的方法,同时我们期待分享更多我们在架构蓝图中进行的实验和改进。
如果您有兴趣了解 Compose 版的蓝图代码,请查看 dev-compose:
https://github.com/android/architecture-samples/tree/dev-compose
如果您想浏览逐步迁移的 PR,请查看以下列表:
统计信息 (Statistics) 屏幕:
https://github.com/android/architecture-samples/pull/821
添加/编辑任务 (Add/Edit task) 屏幕:
https://github.com/android/architecture-samples/pull/823
任务详细信息 (Task detail) 屏幕:
https://github.com/android/architecture-samples/pull/822
任务 (Tasks) 屏幕:
https://github.com/android/architecture-samples/pull/826
以及最终 PR,此操作将 Navigation 和 Activity 逻辑迁移至 Compose,包括移除未使用的 View 系统依赖项:
最终 PR
https://github.com/android/architecture-samples/pull/827移除未使用的 View 系统依赖项
https://github.com/android/architecture-samples/pull/827/commits/2810a37c479ef4b23b4cabf095c55df7b342235e
推荐阅读